package server.proxy;

import javafx.util.Pair;
import server.storage.DiskOperations;
import server.storage.LoginDataService;
import server.storage.PrivateMessagesStorage;
import server.unit.Client;
import server.unit.Message;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.Socket;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class   ClientHandler implements ClientHandlerInterface, Receiverable {
    public static final String GUEST_ROOM = "Guest_room";
    public static final String EMPTY_HISTORY = "Empty history";
    public static final String ALL_HISTORY = "all";

    private MessagesSender messagesSender;

    private Map<String, List<Client>> lobbiesToClientsMap = new HashMap<>();

    private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private Lock read = readWriteLock.readLock();
    private Lock write = readWriteLock.writeLock();

    private String passwordsPath;
    private String privateMessPath;
    private DiskOperations diskOperations;
    private LoginDataService loginDataService;
    private final PrivateMessagesStorage privateMessagesStorage;

    public ClientHandler(String path, int numberOfDays, String passwordsPath, String privateMessPath) throws IOException {
        this.passwordsPath = passwordsPath;
        this.privateMessPath = privateMessPath;
        privateMessagesStorage = new PrivateMessagesStorage(privateMessPath);
        diskOperations = new DiskOperations(path, numberOfDays);
        messagesSender = new MessagesSender(lobbiesToClientsMap, diskOperations, read);
        loginDataService = new LoginDataService(passwordsPath);
    }

    public void messageSenderStart() {
        messagesSender.start();
    }

    @Override
    public void onClientConnected(Socket clientSocket) {
        try {
            Client client = new Client(clientSocket, this);
            client.startListener();
        } catch (IOException e) {
            System.out.println("couldn't connect to client");
        }
    }

    @Override
    public void onReceive(Client client, String message) {
        if(message == null) {
            return;
        }
        if(message.startsWith("/log")) {
            client.write(answerOnCommandLogin(client, message));
            return;
        }
        if(message.startsWith("/reg")) {
            client.write(answerOnCommandRegistration(client, message));
            return;
        }
        if(!client.isLogged()) {
            client.write("ACK Please log in by typing:\n/log <nick> <password>\nor registrate by command\n/reg <login> <password>");
            return;
        }
        if(message.startsWith("/histp")) {
            commandShowPrivateHistory(client);
        } else if(message.startsWith("/hist")) {
            commandShowHistory(client, message);
        } else if(message.startsWith("/chroom")) {
            commandChangeRoom(client, message);
        } else if(message.startsWith("/rooms")) {
            commandShowRooms(client);
        } else if(message.startsWith("/sndp")) {
            commandSendPrivateMessage(client, message);
        } else if(message.startsWith("/snd")) {
            commandSendMessage(client, message);
        } else {
            client.write("ACK Wrong command");
        }
    }

    private void commandShowPrivateHistory(Client client) {
        try {
            List<String> mes = privateMessagesStorage.getPrivate(privateMessPath, client.getNickName(), 100);
            client.write(collectHistory(mes));
        } catch (FileNotFoundException e) {
            client.write("ACK " + EMPTY_HISTORY);
        }
    }

    private void commandSendPrivateMessage(Client sender, String message) {
        String tempMessage = message.substring(5).trim();
        int delim = tempMessage.indexOf(' ');
        if(delim < 0) {
            sender.write("ACK Bad command");
            return;
        }
        String nick = tempMessage.substring(0, delim);
        String info = tempMessage.substring(delim).trim();
        if(!Message.isMessageFit(info)) {
            sender.write("ACK Bad width message");
            return;
        }
        Client receiver = findClientByName(nick);
        if(receiver == null) {
            sender.write("ACK Receiver is not online");
        } else {
            Message mes = new Message(sender, "<private:to " + receiver.getNickName() + ">" + sender.getNickName(), info);
            boolean savingResult;
            synchronized(privateMessagesStorage) {
                savingResult = privateMessagesStorage.saveMessage(
                        sender.getNickName(),
                        receiver.getNickName(),
                        "\n" + mes.toString().substring(4));
            }
            if(savingResult) {
                receiver.write(mes.toString());
                sender.write(mes.toString());
                sender.write("ACK OK");
            } else {
                sender.write("ACK Private message was not sent");
            }
        }
    }

    private Client findClientByName(String nick) {
        read.lock();
        for(List<Client> clients: lobbiesToClientsMap.values()) {
            for(Client client: clients) {
                if(client.getNickName().equals(nick)) {
                    read.unlock();
                    return client;
                }
            }
        }
        read.unlock();
        return null;
    }

    private void commandChangeRoom(Client client, String message) {
        String newRoom = message.substring(7).trim();
        if("".equals(newRoom)) {
            newRoom = GUEST_ROOM;
        }
        String answer;
        if(ALL_HISTORY.equals(newRoom) || !Message.isMessageFit(newRoom)) {
            answer = "ACK wrong room name";
        } else {
            changeRoom(client, newRoom);
            answer = "ACK OK";
        }
        client.write(answer);
    }

    @Override
    public void onClientDisconnect(Client client) {
        removeClient(client);
    }

    private void commandShowRooms(Client client) {
        StringBuilder result = new StringBuilder("ACK List of room names:\n");
        for(String lobby: lobbiesToClientsMap.keySet()) {
            result.append(lobby)
                    .append(" (")
                    .append(lobbiesToClientsMap.get(lobby).size())
                    .append(" user(s))\n");
        }
        client.write(result.toString());
    }

    private void changeRoom(Client client, String roomName) {
        write.lock();
        removeClientFromLobby(client);
        List<Client> clientInLobby = lobbiesToClientsMap.get(roomName);
        if(clientInLobby == null) {
            lobbiesToClientsMap.put(roomName, new ArrayList<Client>());
        }
        client.setLobby(roomName);
        lobbiesToClientsMap.get(roomName).add(client);
        write.unlock();
    }

    private void removeClientFromLobby(Client client) {
        List<Client> clientList = lobbiesToClientsMap.get(client.getLobby());
        if(clientList != null) {
            clientList.remove(client);
            if (clientList.isEmpty()) {
                lobbiesToClientsMap.remove(client.getLobby());
            }
        }
    }

    public void removeClient(Client client) {
        write.lock();
        removeClientFromLobby(client);
        System.out.println("client deleted");
        write.unlock();
    }

    private void commandShowHistory(Client client, String message) {
        String roomName = message.substring(5).trim();
        List<String> messages;
        if(ALL_HISTORY.equals(roomName)) {
            messages = diskOperations.loadAllMessages();
        } else {
            String lobby;
            if("".equals(roomName)) {
                lobby = client.getLobby();
            } else {
                lobby = roomName;
            }
            messages = diskOperations.loadAllMessagesFromLobby(lobby);
        }
        client.write(collectHistory(messages));
    }

    private String collectHistory(List<String> messages) {
        StringBuilder stringBuilder = new StringBuilder("ACK ");
        for(int i = Math.max(0, messages.size() - 100); i < messages.size(); i++) {
            stringBuilder.append(messages.get(i));
        }
        if(stringBuilder.toString().equals("ACK ")) {
            stringBuilder.append(EMPTY_HISTORY);
        }
        return stringBuilder.toString();
    }

    private void commandSendMessage(Client client, String message) {
        message = message.substring(4).trim();
        if(Message.isMessageFit(message)) {
            messagesSender.addMessage(new Message(client, client.getFullNameWithLobby(), message));
        } else {
            client.write("ACK wrong message width");
        }
    }

    private String answerOnCommandLogin(Client client, String message) {
        Pair<String, String> loginAndPassword = parseMessageByLogAndRegCommands(message);
        if(loginAndPassword.getKey() == null || loginAndPassword.getValue().contains(" ")) {
            return "ACK Wrong command";
        }
        String neededPassword;
        try {
            if((neededPassword = LoginDataService.getPasswordByLogin(passwordsPath, loginAndPassword.getKey())) != null) {
                if (neededPassword.equals(loginAndPassword.getValue())) {
                    return getAnswerByUserAdding(client, loginAndPassword.getKey());
                } else {
                    return "ACK Wrong password";
                }
            } else {
                return "ACK User not found, try to register";
            }
        } catch (IOException e) {
            return "ACK Server problem. Couldn't login client";
        }
    }

    private String getAnswerByUserAdding(Client client, String newName) {
        String answer;
        if (findClientByName(newName) == null) {
            if (client.setNickName(newName)) {
                if(client.getLobby() == null) {
                   changeRoom(client, GUEST_ROOM);
                }
                answer = "ACK OK";
            } else {
                answer = "ACK Incorrect name";
            }
        } else {
            answer = "ACK Name already online";
        }
        return answer;
    }

    private String answerOnCommandRegistration(Client client, String message) {
        Pair<String, String> loginAndPassword = parseMessageByLogAndRegCommands(message);
        if(loginAndPassword.getKey() == null) {
            return "ACK Wrong command";
        }
        if(loginAndPassword.getValue().contains(" ")) {
            return "Login or password contains space symbol";
        }
        try {
            if(LoginDataService.getPasswordByLogin(passwordsPath, loginAndPassword.getKey()) == null) {
                if(loginDataService.addNewClient(loginAndPassword.getKey(), loginAndPassword.getValue())) {
                    return getAnswerByUserAdding(client, loginAndPassword.getKey());
                }
                return "ACK Client not registered";
            } else {
                return "ACK User already exists";
            }
        } catch (IOException e) {
            return "ACK Server problem. Couldn't register new client";
        }
    }

    private Pair<String, String> parseMessageByLogAndRegCommands(String message) {
        String newName = message.substring(4).trim();
        int delim = newName.indexOf(' ');
        if(delim < 0) {
            return new Pair<>(null, null);
        }
        String password = newName.substring(delim + 1);
        newName = newName.substring(0, delim);
        return new Pair<>(newName, password);
    }
}
